package com.demo.webapp.kcfinder.freemarker;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.util.Locale;
import java.util.Map;
import java.util.Properties;

import javax.servlet.GenericServlet;
import javax.servlet.ServletContext;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.demo.common.ClassLoaderUtils;
import com.demo.webapp.kcfinder.context.ActionContext;
import com.demo.webapp.kcfinder.servlet.JspSupportServlet;

import freemarker.cache.ClassTemplateLoader;
import freemarker.cache.FileTemplateLoader;
import freemarker.cache.MultiTemplateLoader;
import freemarker.cache.TemplateLoader;
import freemarker.cache.WebappTemplateLoader;
import freemarker.ext.beans.BeansWrapper;
import freemarker.ext.jsp.TaglibFactory;
import freemarker.ext.servlet.AllHttpScopesHashModel;
import freemarker.ext.servlet.HttpRequestHashModel;
import freemarker.ext.servlet.HttpRequestParametersHashModel;
import freemarker.ext.servlet.HttpSessionHashModel;
import freemarker.ext.servlet.ServletContextHashModel;
import freemarker.template.Configuration;
import freemarker.template.ObjectWrapper;
import freemarker.template.SimpleHash;
import freemarker.template.TemplateException;
import freemarker.template.TemplateExceptionHandler;
import freemarker.template.TemplateModel;

/**
 * Freemarker管理器类
 *  <br>提供Freemarker的使用依据:
 *  <br>1.读取基本配置信息
 *  <br>2.创建Freemarker的Configuration
 *  <br>3.创建Freemarker的Template
 *  
 * @author ryuu.kk
 */
public class FreemarkerManager {

    protected static final Log LOG = LogFactory.getLog(FreemarkerManager.class);
    
    //ftl文件中使用该关键字,查询内容
    public static final String KEY_REQUEST = "Request";
    public static final String KEY_INCLUDE = "include_page";
    public static final String KEY_REQUEST_PARAMETERS = "RequestParameters";
    public static final String KEY_SESSION = "Session";
    public static final String KEY_APPLICATION = "Application";
    public static final String KEY_JSP_TAGLIBS = "JspTaglibs";
    public static final String KEY_EXCEPTION = "exception";
    public static final String KEY_REQUEST_PRIVATE = "__FreeMarkerServlet.Request__";
    public static final String KEY_APPLICATION_PRIVATE = "__FreeMarkerServlet.Application__";
    public static final String KEY_LOCALE = "Locale";
    public static final String KEY_THEMES = "Themes";
    
    public static final String CONFIG_SERVLET_CONTEXT_KEY = "freemarker.Configuration";
    public static final String INITPARAM_TEMPLATE_PATH = "templatePath";

    public static final String SETTINGS_FILENAME = "freemarker.properties";
    public static final String CACHE_BEANWAPPRE = "freemarker.cache.beanwapper";
    public static final String DEFAULT_SETTINGS_PATH = "classpath:com/demo/webapp/kcfinder/config/freemarker.properties";
    
    // Note these names start with dot, so they're essentially invisible from  a freemarker script.
    private static final String ATTR_REQUEST_MODEL = ".freemarker.Request";
    private static final String ATTR_REQUEST_PARAMETERS_MODEL = ".freemarker.RequestParameters";
    private static final String ATTR_APPLICATION_MODEL = ".freemarker.Application";
    private static final String ATTR_JSP_TAGLIBS_MODEL = ".freemarker.JspTaglibs";
    
    // for sitemesh
    public static final String ATTR_TEMPLATE_MODEL = ".freemarker.TemplateModel";  // template model stored in request for siteMesh
    
    protected Configuration config;
    protected ObjectWrapper wrapper;
    protected Properties properties;
    protected String contentType = null;
    protected String encoding;
    protected String templatePath;
    protected String templateUpdateDelay;
    protected int mruMaxStrongSize;
    protected boolean nocache;
    protected boolean debug = true;
    protected boolean altMapWrapper;
    protected boolean cacheBeanWrapper;
    protected boolean noCharsetInContentType = true;

    public synchronized Configuration getConfiguration(ServletContext servletContext) throws TemplateException {
        if (config == null || isDebug()) {
            init(servletContext);
            servletContext.setAttribute(CONFIG_SERVLET_CONTEXT_KEY, config);
        }
        return config;
    }
    
    private void init(ServletContext servletContext) throws TemplateException {
    	//读取基本信息
    	this.config = createConfiguration(servletContext);
    	
    	 this.wrapper = createObjectWrapper(servletContext);
    	 
         if (LOG.isDebugEnabled()) {
             LOG.debug("Using object wrapper of class " + wrapper.getClass().getName());
         }
         config.setObjectWrapper(wrapper);
    }
    
    protected Configuration createConfiguration(ServletContext servletContext) throws TemplateException {
        Configuration configuration = new Configuration();

        //环境默认配置,如果freemarker.properties也存在该配置信息,则freemarker.properties文件的配置信息优先
        if (mruMaxStrongSize > 0) {
            configuration.setSetting(Configuration.CACHE_STORAGE_KEY, "strong:" + mruMaxStrongSize);
        }
        if (templateUpdateDelay != null) {
            configuration.setSetting(Configuration.TEMPLATE_UPDATE_DELAY_KEY, templateUpdateDelay);
        }
        if (encoding != null) {
            configuration.setDefaultEncoding(encoding);
        }
        
        configuration.setWhitespaceStripping(true);
        
        configuration.setTemplateExceptionHandler(TemplateExceptionHandler.HTML_DEBUG_HANDLER);

        //环境配置
        if (properties == null || isDebug()) {
        	properties = loadSettings(servletContext);
        }

        templatePath = servletContext.getInitParameter(INITPARAM_TEMPLATE_PATH);
        configuration.setTemplateLoader(createTemplateLoader(servletContext, templatePath));
        
        //初始化配置信息
        configuration.setSettings(properties);
        
        return configuration;
    }
    
    protected ObjectWrapper createObjectWrapper(ServletContext servletContext) {
    	BeansWrapper wrapper = new BeansWrapper();
    	
    	boolean useCache = cacheBeanWrapper;
    	String strCache = (String) this.properties.get(CACHE_BEANWAPPRE);
    	if(StringUtils.isNotEmpty(strCache)) {
    		useCache = "true".equals(strCache.trim());
    	}
    	wrapper.setUseCache(useCache);
    	
    	return wrapper;
    }
    
    protected TemplateLoader createTemplateLoader(ServletContext servletContext, String templatePath) {
        TemplateLoader templatePathLoader = null;

         try {
             if(templatePath != null){
                 if (templatePath.startsWith("class:/")) {
                     templatePathLoader = new ClassTemplateLoader(getClass(), templatePath.substring(7));
                 } else if (templatePath.startsWith("file://")) {
                     templatePathLoader = new FileTemplateLoader(new File(templatePath));
                 } else if (templatePath.startsWith("classpath:/")) {
                	 templatePathLoader = new ClassTemplateLoader(getClass(), templatePath.substring(11));
                 }
             }
         } catch (IOException e) {
             LOG.error("Invalid template path specified: " + e.getMessage(), e);
         }

         return templatePathLoader != null ?
                 new MultiTemplateLoader(new TemplateLoader[]{
                         templatePathLoader,
                         new WebappTemplateLoader(servletContext),
                         new ClassTemplateLoader(getClass(), "/")
                 })
                 : new MultiTemplateLoader(new TemplateLoader[]{
                 new WebappTemplateLoader(servletContext),
                 new ClassTemplateLoader(getClass(), "/")
         });
     }
    
	protected HttpScopesHashModel buildScopesHashModel(ActionContext context, ObjectWrapper wrapper) {
    	
    	ServletContext servletContext = context.getServletContext();
    	HttpServletRequest request = context.getRequest();
    	HttpServletResponse response = context.getResponse();
    	Map<String, Object> resultMap = context.getResult();
    	Locale locale = context.getLocale();
    	String themes = context.getThemes();
    	
    	HttpScopesHashModel model = new HttpScopesHashModel(wrapper, servletContext, request, resultMap);

        // Create hash model wrapper for servlet context (the application). We need one thread, once per servlet context
        synchronized (servletContext) {
            ServletContextHashModel servletContextModel = (ServletContextHashModel) servletContext.getAttribute(ATTR_APPLICATION_MODEL);
            if (servletContextModel == null) {
                // first try a JSP support servlet.  If it fails, oh?
            	// look at JspSupportServlet that static class and configurate in web.xml
                GenericServlet servlet = JspSupportServlet.jspSupportServlet;
                if (servlet != null) {
                    servletContextModel = new ServletContextHashModel(servlet, wrapper);
                    servletContext.setAttribute(ATTR_APPLICATION_MODEL, servletContextModel);
                } else {
                    servletContextModel = new ServletContextHashModel(servletContext, wrapper);
                    servletContext.setAttribute(ATTR_APPLICATION_MODEL, servletContextModel);
                }
                
                TaglibFactory taglibs = new TaglibFactory(servletContext);
                servletContext.setAttribute(ATTR_JSP_TAGLIBS_MODEL, taglibs);
            }
            model.put(KEY_APPLICATION, servletContextModel);
            model.putUnlistedModel(KEY_APPLICATION_PRIVATE, servletContextModel);
        }
        model.put(KEY_JSP_TAGLIBS, (TemplateModel) servletContext.getAttribute(ATTR_JSP_TAGLIBS_MODEL));

        // Create hash model wrapper for session
        HttpSession session = request.getSession(false);
        if (session != null) {
            model.put(KEY_SESSION, new HttpSessionHashModel(session, wrapper));
        }

        // Create hash model wrapper for the request attributes
        HttpRequestHashModel requestModel = (HttpRequestHashModel) request.getAttribute(ATTR_REQUEST_MODEL);

        if ((requestModel == null) || (requestModel.getRequest() != request)) {
            requestModel = new HttpRequestHashModel(request, response, wrapper);
            request.setAttribute(ATTR_REQUEST_MODEL, requestModel);
        }

        model.put(KEY_REQUEST, requestModel);

        // Create hash model wrapper for request parameters
        HttpRequestParametersHashModel reqParametersModel = (HttpRequestParametersHashModel) request.getAttribute(ATTR_REQUEST_PARAMETERS_MODEL);
        if (reqParametersModel == null || requestModel.getRequest() != request) {
            reqParametersModel = new HttpRequestParametersHashModel(request);
            request.setAttribute(ATTR_REQUEST_PARAMETERS_MODEL, reqParametersModel);
        }
        model.put(ATTR_REQUEST_PARAMETERS_MODEL, reqParametersModel);

        // Create hash model wrapper for custom context
        SimpleHash customHash = new SimpleHash();
        customHash.put(KEY_THEMES, themes);
        customHash.put(KEY_LOCALE, locale);
        
        return model;
    }
	
	public HttpScopesHashModel buildTemplateModel(ActionContext context, ObjectWrapper wrapper) {
		HttpServletRequest request = context.getRequest();
		
		HttpScopesHashModel model = buildScopesHashModel(context, wrapper);
		
		// support for JSP exception pages, exposing the servlet or JSP exception
        Throwable exception = (Throwable) request.getAttribute("javax.servlet.error.exception");

        if (exception == null) {
            exception = (Throwable) request.getAttribute("javax.servlet.error.JspException");
        }

        if (exception != null) {
            model.put(KEY_EXCEPTION, exception);
        }
        
        //place the model in the request using the special parameter.  This can be retrieved for freemarker and velocity.
        request.setAttribute(ATTR_TEMPLATE_MODEL, model);
        
        return model;
	}
	
	public TemplateModel buildTemplateModel() {
		ActionContext context = ActionContext.getContext(); 
		ObjectWrapper wrapper = createObjectWrapper(context.getServletContext());
		
		return buildTemplateModel(context, wrapper);
	}
    
    /**
     * 读取Freemarker初始化信息
     * 
     * @param servletContext ServletContext
     * @return Properties
     */
    private synchronized Properties loadSettings(ServletContext servletContext) {
    	
    	if (this.properties != null) {
    		return this.properties;
    	}
    	//使用自定义配置
    	String settings = servletContext.getInitParameter(FreemarkerManager.SETTINGS_FILENAME);
    	InputStream in = null;
    	if (StringUtils.isEmpty(settings)) {
    		in = servletContext.getResourceAsStream(settings);
    	}
    	Properties properties = new Properties();
    	//如果无法得到用户配置信息,则使用默认配置信息
    	if (in == null) {
    		try {
    			in = ClassLoaderUtils.getResourceAsStream(DEFAULT_SETTINGS_PATH, this.getClass());
    			properties.load(in);
    		} catch (IOException e) {
    			// TODO Auto-generated catch block
    			e.printStackTrace();
    		}
    	}
    	
    	return properties;
    }

    //~~ settings here ============================================================================
	public void setMruMaxStrongSize(int mruMaxStrongSize) {
		this.mruMaxStrongSize = mruMaxStrongSize;
	}

	public void setTemplateUpdateDelay(String templateUpdateDelay) {
		this.templateUpdateDelay = templateUpdateDelay;
	}

	public void setContentType(String contentType) {
		this.contentType = contentType;
	}

	public void setEncoding(String encoding) {
		this.encoding = encoding;
	}

	public void setTemplatePath(String templatePath) {
		this.templatePath = templatePath;
	}
	
	public boolean isDebug() {
		return debug;
	}

	public void setDebug(boolean debug) {
		this.debug = debug;
	}

	public Configuration getConfig() {
		return config;
	}
	
	/**
	 * 释放Configuration信息
	 * 该方法适用于Debug模式或容器析构时
	 * 提供FreemarkerManager释放资源
	 */
	public void release() {
		this.config = null;
		this.properties = null;
	}
}
